#!/usr/bin/python
# -*- coding: utf-8 -*-

# This file is part of Cockpit.
#
# Copyright (C) 2015 Red Hat, Inc.
#
# Cockpit is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 2.1 of the License, or
# (at your option) any later version.
#
# Cockpit is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.

import parent
from testlib import *

import os
import unittest
import time
import sys
import re

from kubelib import *

# NOTE: Both TestOpenshift and TestRegistry are in this single file to
# prevent them from being run concurrently.  Both use a 'openshift'
# machine, and we can only run a single one of those at the same time.

def wait_project(machine, project):
    i = 0
    found = True
    while True:
        try:
            output = machine.execute("oc get projects")
            if project not in output:
                if not found:
                    print >> sys.stderr, output
                found = True
                raise Exception(output)
            break
        except:
            if i > 60:
                raise
            i = i + 1
            time.sleep(2)

@skipImage("Kubernetes not packaged", "debian-8", "debian-testing", "ubuntu-1604")
@skipImage("No cockpit-kubernetes packaged", "continuous-atomic", "fedora-atomic", "rhel-atomic")
class TestOpenshift(MachineCase, OpenshiftCommonTests):
    additional_machines = {
        'openshift': { 'machine': { 'image': 'openshift' } }
    }

    def setUp(self):
        super(TestOpenshift, self).setUp()

        self.openshift = self.machines['openshift']
        self.openshift.upload(["verify/files/mock-app-openshift.json"], "/tmp")
        tmpfile = os.path.join(self.tmpdir, "config")
        self.openshift.download("/root/.kube/config", tmpfile)

        m = self.machine
        with open(tmpfile, "r") as f:
            m.execute("mkdir -p /home/admin/.kube && cat > /home/admin/.kube/config", input=f.read())

        wait_project(self.openshift, "marmalade")

        # Expect the default container user limitations during testing
        self.openshift.execute("oc patch scc restricted -p '{ \"runAsUser\": { \"type\": \"MustRunAsRange\" } }'")

        self.browser.wait_timeout(120)

    def testConnect(self):
        m = self.machine
        b = self.browser

        # Make sure we can write to kubeconfig
        m.execute("chown -R admin:admin /home/admin/.kube")
        self.login_and_go("/kubernetes")

        b.wait_present("#service-list")
        b.wait_in_text("#service-list", "docker-registry")
        b.wait_present("a[href='#/volumes']")
        b.click("a[href='#/volumes']")
        b.wait_present(".pv-listing")
        b.wait_in_text(".pv-listing", "No volumes are present")
        b.click("a[href='#/']")

        b.wait_present("#kubernetes-change-connection")
        b.click("#kubernetes-change-connection")
        b.wait_present("modal-dialog")
        b.wait_present("#kubernetes-cluster")
        b.wait_present("#kubernetes-user")
        b.wait_not_present("#kubernetes-username")
        b.wait_not_present("#kubernetes-password")
        b.wait_not_present("#kubernetes-token")

        b.wait_in_text("#kubernetes-cluster button", "10-111-112-101:8443")
        b.wait_in_text("#kubernetes-user button", "system:admin/10-111-112-101:8443")
        b.wait_in_text("modal-dialog", "Client Certificate")

        b.click("#kubernetes-user button")
        b.click("#kubernetes-user ul li:last-child a")
        b.wait_in_text("#kubernetes-user button", "Add New User")
        b.wait_not_present("#kubernetes-token")
        b.wait_val("#kubernetes-username", "")
        b.wait_val("#kubernetes-password", "")
        b.set_val("#kubernetes-username", "new-user")
        b.set_val("#kubernetes-password", "new-user")

        b.click("modal-dialog div.modal-footer button.btn-primary")
        b.wait_not_present("modal-dialog")

        # scruffy isn't a admin
        b.wait_present("#service-list")
        b.wait_not_in_text("#service-list", "docker-registry")
        b.wait_present("a[href='#/volumes']")
        b.click("a[href='#/volumes']")
        b.wait_present(".pv-listing")
        b.wait_in_text(".pv-listing", "cannot list all")
        b.click("a[href='#/']")

        b.wait_present("#kubernetes-change-connection")
        b.click("#kubernetes-change-connection")
        b.wait_present("modal-dialog")
        b.wait_present("#kubernetes-cluster")
        b.wait_present("#kubernetes-user")
        b.wait_not_present("#kubernetes-username")
        b.wait_not_present("#kubernetes-password")
        b.wait_present("#kubernetes-token")

        b.wait_in_text("#kubernetes-cluster button", "10-111-112-101:8443")
        b.wait_in_text("#kubernetes-user button", "new-user/10-111-112-101:8443")
        b.wait_not_in_text("modal-dialog", "Client Certificate")
        self.assertFalse(b.val("#kubernetes-token") == "")

        b.logout()

        # Test the saved kube config file
        m.execute("rm /home/admin/.kube/config")
        m.upload(["verify/files/openshift.kubeconfig"], "/home/admin/.kube/config")
        m.execute("chown -R admin:admin /home/admin/.kube")

        self.login_and_go("/kubernetes")

        b.wait_present("#service-list")
        b.wait_in_text("#service-list", "docker-registry")

    @unittest.skipIf(True, "Nulecule deploys temporarily removed.")
    def testDeployDialog(self):
        b = self.browser
        m = self.machine
        b.wait_timeout(240)
        m.execute("systemctl start docker")
        # m.execute("docker pull submod/helloapache")
        tmpfile = os.path.join(self.tmpdir, "oc")
        self.openshift.download("/usr/bin/oc", tmpfile)
        m.upload([tmpfile], "/usr/local/bin")

        self.login_and_go("/kubernetes")
        b.wait_present("#service-list")
        b.wait_in_text("#service-list", "registry")

        # 1)check atomic version
        output = m.execute("atomic -v 2>&1")
        self.assertTrue(float(output) >= 1.1)

        # 2)check provider is supported
        m.execute("mkdir /var/tmp/invalid-app1")
        m.execute("""echo -e '
FROM busybox
MAINTAINER cockpit
LABEL io.projectatomic.nulecule.atomicappversion="0.1.11" \
      RUN="docker run -it --rm \${OPT1} --privileged -v `pwd`:/atomicapp -v /run:/run -v /:/host --net=host --name \${NAME} -e NAME=\${NAME} -e IMAGE=\${IMAGE} \${IMAGE} -v \${OPT2} run \${OPT3} /atomicapp" \
      STOP="docker run -it --rm \${OPT1} --privileged -v `pwd`:/atomicapp -v /run:/run -v /:/host --net=host --name \${NAME} -e NAME=\${NAME} -e IMAGE=\${IMAGE} \${IMAGE} -v \${OPT2} stop \${OPT3} /atomicapp" \
      INSTALL="docker run -it --rm \${OPT1} --privileged -v `pwd`:/atomicapp -v /run:/run  --name \${NAME} -e NAME=\${NAME} -e IMAGE=\${IMAGE} \${IMAGE} -v \${OPT2} install \${OPT3} --destination /atomicapp /application-entity" \
      io.projectatomic.nulecule.providers="kubernetes" \
      io.projectatomic.nulecule.specversion=0.0.2 \
      io.projectatomic.nulecule.atomicappversion="0.1.11"
' > /var/tmp/invalid-app1/Dockerfile""")
        m.execute("docker build -t test/invalid-app1 /var/tmp/invalid-app1")
        m.execute("rm -rf /var/tmp/invalid-app1")

        b.click("#deploy-app")
        b.wait_popup("deploy-app-dialog")
        b.set_val("#deploy-app-type", "nulecule")
        b.set_val("#deploy-app-nulecule-image", "test/invalid-app1")
        b.set_val("#deploy-app-namespace", "mynamespace")
        b.click("#deploy-app-start")
        b.wait_not_attr("#deploy-app-start", "disabled", "disabled")
        b.is_visible(".modal-footer .alert")
        self.assertEqual(b.text(".modal-footer .alert") ,"No supported providers found.")
        b.dialog_cancel("#deploy-app-dialog")

        # 3)check atomicappversion is supported
        m.execute("mkdir /var/tmp/invalid-app2")
        m.execute("""echo -e '
FROM busybox
MAINTAINER cockpit
LABEL io.projectatomic.nulecule.atomicappversion="0.1.11" \
      RUN="docker run -it --rm \${OPT1} --privileged -v `pwd`:/atomicapp -v /run:/run -v /:/host --net=host --name \${NAME} -e NAME=\${NAME} -e IMAGE=\${IMAGE} \${IMAGE} -v \${OPT2} run \${OPT3} /atomicapp" \
      STOP="docker run -it --rm \${OPT1} --privileged -v `pwd`:/atomicapp -v /run:/run -v /:/host --net=host --name \${NAME} -e NAME=\${NAME} -e IMAGE=\${IMAGE} \${IMAGE} -v \${OPT2} stop \${OPT3} /atomicapp" \
      INSTALL="docker run -it --rm \${OPT1} --privileged -v `pwd`:/atomicapp -v /run:/run  --name \${NAME} -e NAME=\${NAME} -e IMAGE=\${IMAGE} \${IMAGE} -v \${OPT2} install \${OPT3} --destination /atomicapp /application-entity" \
      io.projectatomic.nulecule.providers="kubernetes,openshift" \
      io.projectatomic.nulecule.specversion=0.0.2 \
      io.projectatomic.nulecule.atomicappversion="0.0.11"
' > /var/tmp/invalid-app2/Dockerfile""")
        m.execute("docker build -t test/invalid-app2 /var/tmp/invalid-app2")
        m.execute("rm -rf /var/tmp/invalid-app2")

        b.click("#deploy-app")
        b.wait_popup("deploy-app-dialog")
        b.set_val("#deploy-app-type", "nulecule")
        b.set_val("#deploy-app-nulecule-image", "test/invalid-app2")
        b.set_val("#deploy-app-namespace", "mynamespace")
        b.click("#deploy-app-start")
        b.wait_not_attr("#deploy-app-start", "disabled", "disabled")
        b.is_visible(".modal-footer .alert")
        self.assertEqual(b.text(".modal-footer .alert") ,"atomicapp version 0.0.11 is not supported.")
        b.dialog_cancel("#deploy-app-dialog")


        # 5)check for all metadata
        m.execute("mkdir /var/tmp/invalid-app4")
        m.execute("""echo -e '
FROM busybox
MAINTAINER cockpit
LABEL io.projectatomic.nulecule.providers="kubernetes,openshift"
' > /var/tmp/invalid-app4/Dockerfile""")
        m.execute("docker build -t test/invalid-app4 /var/tmp/invalid-app4")
        m.execute("rm -rf /var/tmp/invalid-app4")

        b.click("#deploy-app")
        b.wait_popup("deploy-app-dialog")
        b.set_val("#deploy-app-type", "nulecule")
        b.set_val("#deploy-app-nulecule-image", "test/invalid-app4")
        b.set_val("#deploy-app-namespace", "mynamespace")
        b.click("#deploy-app-start")
        b.wait_not_attr("#deploy-app-start", "disabled", "disabled")
        b.is_visible(".modal-footer .alert")
        self.assertEqual(b.text(".modal-footer .alert") ,"This image is not a supported Nulecule image")
        b.dialog_cancel("#deploy-app-dialog")


        # 6)check when atomicapp is not available
        m.execute("mkdir /var/tmp/invalid-app5")
        m.execute("""echo -e '
FROM busybox
MAINTAINER cockpit
LABEL io.projectatomic.nulecule.atomicappversion="0.1.11" \
      RUN="docker run -it --rm \${OPT1} --privileged -v `pwd`:/atomicapp -v /run:/run -v /:/host --net=host --name \${NAME} -e NAME=\${NAME} -e IMAGE=\${IMAGE} \${IMAGE} -v \${OPT2} run \${OPT3} /atomicapp" \
      STOP="docker run -it --rm \${OPT1} --privileged -v `pwd`:/atomicapp -v /run:/run -v /:/host --net=host --name \${NAME} -e NAME=\${NAME} -e IMAGE=\${IMAGE} \${IMAGE} -v \${OPT2} stop \${OPT3} /atomicapp" \
      INSTALL="docker run -it --rm \${OPT1} --privileged -v `pwd`:/atomicapp -v /run:/run  --name \${NAME} -e NAME=\${NAME} -e IMAGE=\${IMAGE} \${IMAGE} -v \${OPT2} install \${OPT3} --destination /atomicapp /application-entity" \
      io.projectatomic.nulecule.providers="kubernetes,openshift" \
      io.projectatomic.nulecule.specversion=0.0.2 \
      io.projectatomic.nulecule.atomicappversion="0.1.11"
' > /var/tmp/invalid-app5/Dockerfile""")
        m.execute("docker build -t test/invalid-app5 /var/tmp/invalid-app5")
        m.execute("rm -rf /var/tmp/invalid-app5")

        b.click("#deploy-app")
        b.wait_popup("deploy-app-dialog")
        b.set_val("#deploy-app-type", "nulecule")
        b.set_val("#deploy-app-nulecule-image", "test/invalid-app5")
        b.set_val("#deploy-app-namespace", "mynamespace")
        b.click("#deploy-app-start")
        b.wait_not_attr("#deploy-app-start", "disabled", "disabled")
        b.is_visible(".modal-footer .alert")
        self.assertEqual(b.text(".modal-footer .alert") ,"Image failed to install.")
        b.dialog_cancel("#deploy-app-dialog")

        # 7) fail when Unable to pull Nulecule app
        b.click("#deploy-app")
        b.wait_popup("deploy-app-dialog")
        b.set_val("#deploy-app-type", "nulecule")
        b.set_val("#deploy-app-nulecule-image", "submod/helloapache1")
        b.set_val("#deploy-app-namespace", "mynamespace")
        b.click("#deploy-app-start")
        b.wait_not_attr("#deploy-app-start", "disabled", "disabled")
        b.is_visible(".modal-footer .alert")
        self.assertEqual(b.text(".modal-footer .alert") ,"Unable to pull Nulecule app image.")
        b.dialog_cancel("#deploy-app-dialog")

        # 8) check if app can be deployed
        b.click("#deploy-app")
        b.wait_popup("deploy-app-dialog")
        b.set_val("#deploy-app-type", "nulecule")
        b.set_val("#deploy-app-nulecule-image", "submod/helloapache:0.1.11")
        b.set_val("#deploy-app-namespace", "mynamespace")
        b.click("#deploy-app-start")
        self.allow_journal_messages('Could not find any image matching "submod/helloapache:0.1.11".')
        b.wait_not_attr("#deploy-app-start", "disabled", "disabled")
        b.click("#deploy-app-start")
        b.wait_popdown("deploy-app-dialog")
        b.click("a[href='#/list']")
        b.wait_present("#content .details-listing")
        b.wait_present(".details-listing tbody[data-id='pods/default/helloapache'] th")
        self.assertEqual(b.text(".details-listing tbody[data-id='pods/default/helloapache'] th"), "helloapache")

@skipImage("Kubernetes not packaged", "debian-8", "debian-testing", "ubuntu-1604")
@skipImage("No cockpit-kubernetes packaged", "continuous-atomic", "fedora-atomic", "rhel-atomic")
class TestRegistry(MachineCase):
    additional_machines = {
        'openshift': { 'machine': { 'image': 'openshift' } }
    }

    def setUp(self):
        super(TestRegistry, self).setUp()

        self.openshift = self.machines['openshift']

        # Sync over the kube config file
        tmpfile = os.path.join(self.tmpdir, "config")
        self.openshift.download("/root/.kube/config", tmpfile)
        with open(tmpfile, "r") as f:
            self.machine.execute("mkdir -p /home/admin/.kube && cat > /home/admin/.kube/config", input=f.read())
        wait_project(self.openshift, "marmalade")

        self.browser.wait_timeout(120)

    def testImages(self):
        b = self.browser
        o = self.openshift

        self.login_and_go("/kubernetes/registry")
        b.wait_present(".dashboard-images")

        # The default view should be overwhelmed with pizzazz images
        b.wait_in_text(".card-pf-wide.dashboard-images", "pizzazz/monster")
        b.wait_not_in_text(".card-pf-wide.dashboard-images", "default/busybox")
        b.wait_not_in_text(".card-pf-wide.dashboard-images", "marmalade/busybee")
        b.wait_not_in_text(".card-pf-wide.dashboard-images", "marmalade/juggs")
        b.wait_not_in_text(".card-pf-wide.dashboard-images", "marmalade/origin")

        # Filter the dashboard to marmalide project
        b.click(".dashboard-images .namespace-filter button")
        b.wait_visible(".dashboard-images .namespace-filter .dropdown-menu")
        b.wait_present(".dashboard-images .namespace-filter a[value='marmalade']")
        b.click(".dashboard-images .namespace-filter a[value='marmalade']")
        b.wait_not_in_text(".card-pf-wide.dashboard-images", "pizzazz/")
        b.wait_in_text(".card-pf-wide.dashboard-images", "marmalade/busybee")

        # Lets navigate to an image stream
        b.click("a[href='#/images/marmalade/busybee']")
        b.wait_in_text(".content-filter h3", "marmalade/busybee")
        b.click("tbody[data-id='marmalade/busybee:0.x'] tr td.listing-ct-toggle")
        b.wait_present("tbody[data-id='marmalade/busybee:0.x'] .listing-ct-panel dl.registry-image-tags")
        b.wait_in_text("tbody[data-id='marmalade/busybee:0.x'] .listing-ct-panel dl.registry-image-tags", "marmalade/busybee:0.x")

        # Look at the image layers
        b.click(".listing-ct-head li:last-child a")
        b.wait_present(".listing-ct-body .registry-image-layers")
        b.wait_visible(".listing-ct-body .registry-image-layers")
        b.wait_in_text(".listing-ct-body .registry-image-layers", "ADD file:")

        # Add postgres into the stream
        output = o.execute("oc get imagestream --namespace=marmalade --template='{{.spec}}' busybee")
        self.assertNotIn("postgres", output)
        b.click(".pficon-edit")
        b.wait_present("modal-dialog")
        b.wait_visible("#imagestream-modify-populate")
        b.click("#imagestream-modify-populate button")
        b.wait_visible("#imagestream-modify-populate .dropdown-menu")
        b.click("#imagestream-modify-populate a[value='pull']")
        b.wait_visible("#imagestream-modify-pull")
        b.set_val("#imagestream-modify-pull", "postgres")
        b.click(".btn-primary")
        b.wait_not_present("modal-dialog")
        b.wait_in_text ("#content", "postgres")
        output = o.execute("oc get imagestream --namespace=marmalade --template='{{.spec}}' busybee")
        self.assertIn("postgres", output)

        # Remove postgres from the stream
        b.click(".pficon-edit")
        b.wait_present("modal-dialog")
        b.wait_visible("#imagestream-modify-populate")
        b.click("#imagestream-modify-populate button")
        b.wait_visible("#imagestream-modify-populate .dropdown-menu")
        b.click("#imagestream-modify-populate a[value='none']")
        b.click(".btn-primary")
        b.wait_not_present("modal-dialog")
        b.wait_not_in_text ("#content", "postgres")
        output = o.execute("oc get imagestream --namespace=marmalade --template='{{.spec}}' busybee")
        self.assertNotIn("postgres", output)

        # Go to the images view and create a new imagestream
        b.click("#content a[href='#/images/marmalade']")
        b.wait_present("a i.pficon-add-circle-o")
        b.click("a i.pficon-add-circle-o")
        b.wait_present("modal-dialog")
        b.wait_visible("#imagestream-modify-name")
        b.set_val("#imagestream-modify-name", "zero")
        b.wait_val("#imagestream-modify-project-text", "marmalade")
        b.click("#imagestream-modify-project button")
        b.wait_visible("#imagestream-modify-project .dropdown-menu")
        b.click("#imagestream-modify-project a[value='default']")
        b.wait_val("#imagestream-modify-project-text", "default")
        b.set_val("#imagestream-modify-project-text", "###")
        b.click(".btn-primary")
        b.wait_visible(".dialog-error")
        b.set_val("#imagestream-modify-project-text", "default")
        b.click(".btn-primary")
        b.wait_not_present("modal-dialog")

        # Switch to the default namespace and look for what we created
        b.click("filter-bar .namespace-filter button")
        b.wait_visible("filter-bar .namespace-filter .dropdown-menu")
        b.click("filter-bar .namespace-filter a[value='default']")
        b.wait_visible("tbody[data-id='default/zero']")

        # Go to the images view and check annotations
        b.wait_present("tbody[data-id='default/busybox']")
        b.click("tbody[data-id='default/busybox'] th")
        b.wait_present(".content-filter h3")
        b.wait_in_text(".content-filter h3", "default/busybox")
        b.wait_in_text("#content", "Annotations")
        b.wait_in_text("registry-imagestream-meta", "openshift.io/image.dockerRepositoryCheck")

        # Delete the tagged image from its own screen
        b.go("#/images/marmalade/busybee:0.x")
        b.wait_in_text(".content-filter h3", "marmalade/busybee:0.x")
        b.click(".pficon-delete")
        b.wait_present("modal-dialog")
        b.click("modal-dialog .btn-danger")
        b.wait_not_present("modal-dialog")

        # Should redirect to the imagestream page
        b.wait_in_text(".content-filter", "Show all image streams")
        b.wait_not_in_text("#content", "0.x")

        # Delete via the main UI
        b.wait_present("tbody[data-id='marmalade/busybee:latest']")
        b.click("tbody[data-id='marmalade/busybee:latest'] tr.listing-ct-item td.listing-ct-toggle")
        b.wait_present("tbody[data-id='marmalade/busybee:latest'] .listing-ct-panel dl.registry-image-tags")
        b.wait_in_text("tbody[data-id='marmalade/busybee:latest'] .listing-ct-panel dl.registry-image-tags", "marmalade/busybee:latest")
        b.click("tbody[data-id='marmalade/busybee:latest'] .listing-ct-head .pficon-delete")
        b.wait_present("modal-dialog")
        b.click("modal-dialog .btn-danger")
        b.wait_not_present("modal-dialog")

        # All tags here have been removed
        b.wait_not_in_text("#content", "latest")

        # Show the image on the right screen
        b.go("#/images/marmalade/juggs")
        b.wait_in_text(".content-filter h3", "marmalade/juggs")
        b.wait_present("tbody[data-id='marmalade/juggs:2.9']")
        b.click("tbody[data-id='marmalade/juggs:2.9'] tr.listing-ct-item td.listing-ct-toggle")

        # Various labels should show up in this image
        b.wait_in_text("tbody[data-id='marmalade/juggs:2.9'] .listing-ct-panel", "Juggs Image")
        b.wait_in_text("tbody[data-id='marmalade/juggs:2.9'] registry-image-body", "This is a test description of an image. It can be as long as a paragraph, featuring a nice brogrammer sales pitch.")
        b.wait_in_text("tbody[data-id='marmalade/juggs:2.9'] registry-image-body", "http://hipsum.co")

        # And some key labels shouldn't show up on the metadata
        b.click("tbody[data-id='marmalade/juggs:2.9'] .listing-ct-head li:last-child a")
        b.wait_present("tbody[data-id='marmalade/juggs:2.9'] registry-image-meta")
        b.wait_in_text("tbody[data-id='marmalade/juggs:2.9'] registry-image-meta", "build-date=2016-03-04")

    def testProjectGroups(self):
        b = self.browser

        self.login_and_go("/kubernetes/registry")
        b.go("#/projects")
        b.wait_present("tbody[data-id='marmalade']")

        # Create a new group
        b.click("#add-group")
        b.wait_present("modal-dialog")
        b.wait_visible(".modal-body")
        b.set_val("#group_name", "production")
        b.click(".btn-primary")
        b.wait_not_present("modal-dialog")
        b.wait_present("tbody[data-id='marmalade']")

        #group page
        b.click("tbody[data-id='production'] tr:first-child td:nth-of-type(2)")
        b.wait_in_text(".content-filter h3", "production")

        #add member
        b.click("a i.pficon-add-circle-o")
        b.wait_present("modal-dialog")
        b.wait_visible("#add_user_to_group")
        b.click("#add_user_to_group button")
        b.wait_visible("#add_user_to_group .dropdown-menu")
        b.click(".dropdown-menu a[value='scruffy']")
        b.click(".btn-primary")
        b.wait_not_present("modal-dialog")

        #delete member
        b.click("tbody[data-id='scruffy'] tr td:last-child a i.pficon-close")
        b.wait_present("modal-dialog")
        b.wait_visible(".modal-body")
        b.click(".btn-primary")
        b.wait_not_present("modal-dialog")

        b.click("a i.pficon-add-circle-o")
        b.wait_present("modal-dialog")
        b.wait_visible("#add_user_to_group")
        b.click("#add_user_to_group button")
        b.wait_visible("#add_user_to_group .dropdown-menu")
        b.click(".dropdown-menu a[value='scruffy']")
        b.click(".btn-primary")
        b.wait_not_present("modal-dialog")

        #delete user
        b.wait_in_text(".content-filter h3", "production")
        b.click(".content-filter .pficon-delete")
        b.wait_present("modal-dialog")
        b.click(".btn-primary")
        b.wait_not_present("modal-dialog")
        b.wait_not_present("tbody[data-id='production']")

    def testProjectUsers(self):
        o = self.openshift
        b = self.browser

        self.login_and_go("/kubernetes/registry")
        b.go("#/projects")
        o.execute("oc get projects")
        o.execute("oc get rolebinding -n marmalade")
        b.wait_present("tbody[data-id='default']")

        # Create a new project
        b.click("#add-project")
        b.wait_present("modal-dialog")
        b.wait_visible(".modal-body")
        b.wait_visible("#project-new-name")
        b.set_val("#project-new-name", "testprojectuserproj")
        b.click(".btn-primary")
        b.wait_not_present("modal-dialog")

        #wait for it
        b.wait_present("tbody[data-id='testprojectuserproj']")
        o.execute("oc get projects")

        # Create a new user
        b.click("#add-user")
        b.wait_present("modal-dialog")
        b.wait_visible(".modal-body")
        b.wait_visible("#identities")
        b.set_val("#user_name", "testprojectuser")
        b.set_val("#identities", "anypassword:abc123")
        b.click(".btn-primary")
        b.wait_not_present("modal-dialog")

        #wait for it
        b.wait_present("tbody[data-id='testprojectuser']")

        #goto user page
        b.click("tbody[data-id='testprojectuser'] tr:first-child td:nth-of-type(2)")
        b.wait_in_text(".content-filter h3", "testprojectuser")

        #modify user
        b.click(".content-filter .pficon-edit")
        b.wait_present("modal-dialog")
        b.wait_visible("#identities")
        b.set_val("#identities", "anypassword:abc1234")
        b.click(".btn-primary")
        b.wait_not_present("modal-dialog")
        b.wait_in_text(".user-body dd", "anypassword:abc1234")

        #add project member
        b.click("a i.pficon-add-circle-o")
        b.wait_present("modal-dialog")
        b.wait_visible("#add_parent_for_user")
        b.click("#add_parent_for_user button")
        b.wait_visible("#add_parent_for_user .dropdown-menu")
        b.click(".dropdown-menu a[value='testprojectuserproj']")
        b.wait_visible("#add_role_for_user")
        b.click("#add_role_for_user button")
        b.wait_visible("#add_role_for_user .dropdown-menu")
        b.click("#add_role_for_user a[value='Admin']")
        b.click(".btn-primary")
        b.wait_not_present("modal-dialog")

        #delete project member X
        b.click("tbody[data-id='testprojectuserproj'] tr td:last-child a i.pficon-close")
        b.wait_present("modal-dialog")
        b.wait_visible(".modal-body")
        b.click(".btn-primary")
        b.wait_not_present("modal-dialog")

        #add project member again
        b.click("a i.pficon-add-circle-o")
        b.wait_present("modal-dialog")
        b.wait_visible("#add_parent_for_user")
        b.click("#add_parent_for_user button")
        b.wait_visible("#add_parent_for_user .dropdown-menu")
        b.click(".dropdown-menu a[value='testprojectuserproj']")
        b.wait_visible("#add_role_for_user")
        b.click("#add_role_for_user button")
        b.wait_visible("#add_role_for_user .dropdown-menu")
        b.click("#add_role_for_user a[value='Admin']")
        b.click(".btn-primary")
        b.wait_not_present("modal-dialog")

        #add another role to project member
        b.wait_present("tbody[data-id='testprojectuserproj']")
        b.wait_present("tbody[data-id='testprojectuserproj'] tr .btn-group")
        b.wait_visible("tbody[data-id='testprojectuserproj'] tr .btn-group")
        b.click("tbody[data-id='testprojectuserproj'] tr .btn-group button")
        b.wait_visible("tbody[data-id='testprojectuserproj'] tr .btn-group .dropdown-menu")
        b.click("tbody[data-id='testprojectuserproj'] tr .dropdown-menu a[value='Push']")
        b.wait_present("modal-dialog")
        b.wait_visible(".modal-body")
        b.click(".btn-primary")
        b.wait_not_present("modal-dialog")
        b.wait_present("table.listing-ct")
        wait(lambda: re.search(r"registry-editor\s+/registry-editor\s+testprojectuser\b",
                               o.execute("oc get rolebinding -n testprojectuserproj")))

        #delete user
        b.wait_in_text(".content-filter h3", "testprojectuser")
        b.click(".content-filter .pficon-delete")
        b.wait_present("modal-dialog")
        b.click(".btn-primary")
        b.wait_not_present("modal-dialog")

        # HACK: In order to test issue, log the output to the journal
        wait(lambda: not re.search(r"\btestprojectuser\b", o.execute("oc get rolebinding -n testprojectuserproj | logger -s 2>&1")))

        #add/remove members for other roles
        b.go("#/projects/testprojectuserproj")
        for (role, perm) in [("Push", "editor"), ("Pull", "viewer")]:
            username = "testprojectuser" + role.lower()
            b.click("a i.pficon-add-circle-o")
            b.wait_present("modal-dialog")
            b.wait_visible("#add_member_name")
            b.set_val("#add_member_name", username)
            b.wait_visible("#add_role")
            b.click("#add_role button")
            b.wait_visible("#add_role .dropdown-menu")
            b.click("#add_role a[value='%s']" % role)
            b.click(".btn-primary")
            b.wait_not_present("modal-dialog")
            b.wait_present("tbody[data-id='%s']" % username)

            wait(lambda: username in o.execute("oc get rolebinding -n testprojectuserproj"), delay=5)
            output = o.execute("oc get rolebinding -n testprojectuserproj")
            self.assertRegexpMatches(output, "registry-%s\s+/registry-%s\s.*\\b%s\\b" % (perm, perm, username))
            self.assertNotRegexpMatches(output, "registry-admin.*%s" % username)

            b.wait_present("tbody[data-id='%s']" % username)
            b.click("tbody[data-id='%s'] a i.pficon-close" % username)
            b.wait_present("modal-dialog")
            b.click(".btn-primary")
            b.wait_not_present("modal-dialog")
            b.wait_present("table.listing-ct")
            wait(lambda: username not in o.execute("oc get rolebinding -n testprojectuserproj"), delay=5)

        # try to add user with invalid name from testprojectuserproj page
        b.go("#/projects/testprojectuserproj")
        b.wait_present("a i.pficon-add-circle-o")
        b.click("a i.pficon-add-circle-o")
        b.wait_present("modal-dialog")
        b.wait_visible("#add_member_name")
        b.set_val("#add_member_name", "foo ^ bar")
        b.wait_visible("#add_role")
        b.click("#add_role button")
        b.wait_visible("#add_role .dropdown-menu")
        b.click("#add_role a[value='Admin']")
        b.click(".btn-primary")
        self.assertEqual(b.text(".dialog-error"), "The member name contains invalid characters.")

        # but email-style user name should be accepted
        b.set_val("#add_member_name", "foo@bar.com")
        b.click(".btn-primary")
        b.wait_not_present("modal-dialog")
        b.wait_present("tbody[data-id='foo@bar.com']")
        wait(lambda: 'foo@bar.com' in o.execute("oc get rolebinding -n testprojectuserproj"), delay=5)
        self.assertNotIn('foo ^ bar', o.execute("oc get rolebinding -n testprojectuserproj"))

        # it appears on the "All projects" page too
        b.go("#/projects/")
        b.wait_present("tbody[data-id='foo@bar.com']")

        # try to add user with invalid name from "All projects" page
        b.click("#add-user")
        b.wait_present("modal-dialog")
        b.wait_visible(".modal-body")
        b.wait_visible("#identities")
        b.set_val("#user_name", "bar ^ baz")
        b.set_val("#identities", "anypassword:abc123")
        b.click(".btn-primary")
        self.assertEqual(b.text(".dialog-error"), "The name contains invalid characters")

        # email-style user name should be accepted
        b.set_val("#user_name", "bar@baz.com")
        b.click(".btn-primary")
        b.wait_not_present("modal-dialog")
        b.wait_present("tbody[data-id='bar@baz.com']")

    def testProjectPolicy(self):
        o = self.openshift
        b = self.browser

        self.login_and_go("/kubernetes/registry")
        b.wait_present(".dashboard-images")
        b.go("#/projects")

        #wait for it
        b.wait_present("tbody[data-id='default']")

        # Create a new project
        b.click("#add-project")
        b.wait_present("modal-dialog")
        b.wait_visible(".modal-body")
        b.wait_visible("#project-new-name")
        b.set_val("#project-new-name", "testprojectpolicyproj")
        b.click(".btn-primary")
        b.wait_not_present("modal-dialog")

        #wait for it
        b.wait_present("tbody[data-id='testprojectpolicyproj']")
        o.execute("oc get projects")

        #goto project page
        b.click("tbody[data-id='testprojectpolicyproj'] tr:first-child td:nth-of-type(2)")
        b.wait_in_text(".content-filter h3", "testprojectpolicyproj")

        #add user with role
        b.wait_present("a i.pficon-add-circle-o")
        b.click("a i.pficon-add-circle-o")
        b.wait_present("modal-dialog")
        b.wait_visible("#add_member_group")
        b.click("#add_member_group button")
        b.wait_visible("#add_member_group .dropdown-menu")
        b.click("#add_member_group a[value='scruffy']")
        b.click("#add_role button")
        b.wait_visible("#add_role .dropdown-menu")
        b.click("#add_role a[value='Admin']")
        b.click(".btn-primary")
        b.wait_not_present("modal-dialog")

        b.wait_present(".inner-project-listing")
        b.wait_present("a i.pficon-add-circle-o")
        b.click("a i.pficon-add-circle-o")
        b.wait_present("modal-dialog")
        b.wait_visible("#add_member_group")
        b.click(".btn-primary")
        b.wait_present("modal-dialog")
        self.assertEqual(b.text(".dialog-error") ,"Please select a valid Member.")
        b.click(".btn-cancel")
        b.wait_not_present("modal-dialog")

        b.wait_present(".inner-project-listing")
        b.wait_present("a i.pficon-add-circle-o")
        b.click("a i.pficon-add-circle-o")
        b.wait_present("modal-dialog")
        b.wait_visible("#add_member_group")
        b.click("#add_member_group button")
        b.wait_visible("#add_member_group .dropdown-menu")
        b.click("#add_member_group a[value='scruffy']")
        b.click(".btn-primary")
        b.wait_present("modal-dialog")
        self.assertEqual(b.text(".dialog-error") ,"Please select a valid Role.")
        b.click(".btn-cancel")
        b.wait_not_present("modal-dialog")

        # Add a non-existent user
        b.wait_present(".inner-project-listing")
        b.wait_present("a i.pficon-add-circle-o")
        b.click("a i.pficon-add-circle-o")
        b.wait_present("modal-dialog")
        b.wait_visible("#add_member_group")
        b.set_val("#add_member_name", "randomuser")
        b.click("#add_role button")
        b.wait_visible("#add_role .dropdown-menu")
        b.click("#add_role a[value='Admin']")
        b.click(".btn-primary")
        b.wait_not_present("modal-dialog")

        # Add a non-existent user, negative case
        b.wait_present(".inner-project-listing")
        b.wait_present("a i.pficon-add-circle-o")
        b.click("a i.pficon-add-circle-o")
        b.wait_present("modal-dialog")
        b.wait_visible("#add_member_group")
        b.set_val("#add_member_name", "")
        b.click("#add_role button")
        b.wait_visible("#add_role .dropdown-menu")
        b.click("#add_role a[value='Admin']")
        b.click(".btn-primary")
        b.wait_present("modal-dialog")
        self.assertEqual(b.text(".dialog-error") ,"Please select a valid Member.")
        b.click(".btn-cancel")
        b.wait_not_present("modal-dialog")

    def testProjectAdmin(self):
        o = self.openshift
        b = self.browser
        m = self.machine

        # Log in as scruffy
        tmpfile = os.path.join(self.tmpdir, "scruffy")
        o.execute('printf "scruffy\r\nscruffy\r\n" | oc login')
        o.download("/root/.kube/config", tmpfile)
        with open(tmpfile, "r") as f:
            m.execute("mkdir -p /home/admin/.kube && cat > /home/admin/.kube/config", input=f.read())

        self.login_and_go("/kubernetes/registry")

        # Make sure the default view is not visible to non cluster admins
        b.wait_present(".dashboard-images")
        b.wait_visible(".dashboard-images:nth-child(1)")
        b.wait_not_in_text(".card-pf-wide.dashboard-images", "default/busybox")

        # Show that the project displays shared access data
        b.wait_present("tr[data-name='marmalade']")
        b.wait_present("tr[data-name='marmalade'] .fa-lock")

        # Change the project access
        b.go("#/projects/marmalade")
        b.wait_in_text(".content-filter h3", "marmalade")
        b.wait_in_text(".listing-ct-body", "Project access policy only allows specific members to access images. Grant access to specific members below.")
        b.click(".content-filter .pficon-edit")
        b.wait_present("modal-dialog")
        b.wait_visible("#project-access-policy")
        b.wait_in_text("#project-access-policy button", "Private: Allow only specific users or groups to pull images")
        b.click("#project-access-policy button")
        b.wait_visible("#project-access-policy .dropdown-menu")
        b.click("#project-access-policy a[value='shared']")
        b.click(".btn-primary")
        b.wait_not_present("modal-dialog")
        b.wait_not_in_text(".listing-ct-body", "Project access policy allows all authenticated users to pull images. Grant additional access to specific members below.")
        output = o.execute("oc policy who-can get --namespace=marmalade imagestreams/layers")
        self.assertIn("system:authenticated", output)
        self.assertNotIn("system:unauthenticated", output)

        # Look for change in state
        b.go("#/")
        b.wait_present("tr[data-name='marmalade'] .fa-unlock-alt")

        # Change project to shared
        b.go("#/projects/marmalade")
        b.wait_in_text(".content-filter h3", "marmalade")
        b.click(".content-filter .pficon-edit")
        b.wait_present("modal-dialog")
        b.wait_visible("#project-access-policy")
        b.wait_in_text("#project-access-policy button", "Shared: Allow any authenticated user to pull images")
        b.click("#project-access-policy button")
        b.wait_visible("#project-access-policy .dropdown-menu")
        b.click("#project-access-policy a[value='anonymous']")
        b.click(".btn-primary")
        b.wait_not_present("modal-dialog")
        b.wait_in_text(".listing-ct-body", "Project access policy allows anonymous users to pull images. Grant additional push or admin access to specific members below.")
        output = o.execute("oc policy who-can get --namespace=marmalade imagestreams/layers")
        self.assertIn("system:unauthenticated", output)

        # Look for change in state
        b.go("#/")
        b.wait_present("tr[data-name='marmalade'] .fa-unlock")

        # New project doesn't exist
        b.go("#/")
        b.wait_present(".dashboard-images")
        output = o.execute("oc get projects")
        self.assertNotIn("llama", output)
        b.wait_not_in_text(".dashboard-images:first-child", "llama")

        # Create a new project
        b.wait_visible("a.new-project-link")
        b.click("a.new-project-link")
        b.wait_present("modal-dialog")
        b.wait_visible("#project-new-name")
        b.set_val("#project-new-name", "invalid...!")
        b.click(".btn-primary")
        b.wait_visible(".dialog-error")
        b.set_val("#project-new-name", "llama")
        b.set_val("#project-new-display", "Display llama")
        b.set_val("#project-new-description", "Description goes here")
        b.click(".btn-primary")
        b.wait_not_present("modal-dialog")

        # Check that the projcet exists
        b.wait_in_text(".dashboard-images:first-child", "llama")

        # Go and modify the project
        b.go("#/projects")
        b.wait_present("tbody[data-id='llama']")
        b.wait_present("tbody[data-id='llama'] tr.listing-ct-item")
        b.click("tbody[data-id='llama'] tr.listing-ct-item td:nth-of-type(2)")
        b.wait_in_text(".content-filter h3", "Display llama (llama)")
        b.wait_in_text("#content", "Description goes here")
        b.click(".pficon-edit")
        b.wait_present("modal-dialog")
        b.wait_visible("#project-new-display")
        b.set_val("#project-new-display", "What the llama say")
        b.wait_visible("#project-new-description")
        b.set_val("#project-new-description", "Blearrrrrrrrgh")
        b.click(".btn-primary")
        b.wait_not_present("modal-dialog")
        b.wait_in_text(".content-filter h3", "What the llama say (llama)")
        b.wait_in_text("#content", "Blearrrrrrrrgh")

        # Make sure it showed up in the console
        wait_project(o, "llama");

    def testDockerCommandInfo(self):
        o = self.openshift
        b = self.browser
        m = self.machine

        # create push and pull user and login as pushuser
        o.execute("oadm policy add-role-to-user registry-viewer pulluser -n marmalade")
        o.execute("oadm policy add-role-to-user registry-editor pushuser -n marmalade")
        o.execute("oadm policy add-role-to-user registry-viewer pushuser -n pizzazz")
        tmpfile = os.path.join(self.tmpdir, "kubeconfig")
        o.execute('printf "pushuser\r\na\r\n" | oc login')
        o.download("/root/.kube/config", tmpfile)
        with open(tmpfile, "r") as f:
            m.execute("mkdir -p /home/admin/.kube && cat > /home/admin/.kube/config", input=f.read())

        self.login_and_go("/kubernetes/registry")

        # always visible on "All projects" page
        b.wait_in_text("body", "Pull an image")
        b.wait_visible('#docker-push-commands')
        b.wait_visible('#docker-pull-commands')

        # push user should not see docker push command on pizzazz overview page (only a viewer there)
        b.wait_visible(".dashboard-images .namespace-filter")
        b.click(".dashboard-images .namespace-filter button")
        b.wait_visible(".dashboard-images .namespace-filter .dropdown-menu")
        b.wait_present(".dashboard-images .namespace-filter a[value='pizzazz']")
        b.click(".dashboard-images .namespace-filter a[value='pizzazz']")
        b.wait_visible('#docker-pull-commands')
        b.wait_not_visible('#docker-push-commands')

        # push user should see docker push and pull commands on marmalade overview page
        b.click(".dashboard-images .namespace-filter button")
        b.wait_visible(".dashboard-images .namespace-filter .dropdown-menu")
        b.wait_present(".dashboard-images .namespace-filter a[value='marmalade']")
        b.click(".dashboard-images .namespace-filter a[value='marmalade']")
        b.wait_visible('#docker-push-commands')

        # .. and also on the image page
        b.go("#/images/marmalade/origin")
        b.wait_in_text("body", "push to an image to this image stream")
        b.wait_in_text("body", "docker tag")
        b.wait_in_text("body", "docker push")
        b.wait_visible('.registry-imagestream-push')

        # log in as pulluser
        b.logout()
        o.execute('printf "pulluser\r\na\r\n" | oc login')
        o.download("/root/.kube/config", tmpfile)
        with open(tmpfile, "r") as f:
            m.execute("cat > /home/admin/.kube/config", input=f.read())
        self.login_and_go("/kubernetes/registry")

        # always visible on "All projects" page
        b.wait_in_text("body", "Pull an image")
        b.wait_visible('#docker-push-commands')
        b.wait_visible('#docker-pull-commands')

        # pull user should only see docker pull command, but not push on project specific overview page
        b.wait_visible(".dashboard-images .namespace-filter")
        b.click(".dashboard-images .namespace-filter button")
        b.wait_visible(".dashboard-images .namespace-filter .dropdown-menu")
        b.wait_present(".dashboard-images .namespace-filter a[value='marmalade']")
        b.click(".dashboard-images .namespace-filter a[value='marmalade']")
        b.wait_visible('#docker-pull-commands')
        b.wait_not_visible('#docker-push-commands')

        # and neither the push command on the image page
        b.go("#/images/marmalade/origin")
        b.wait_in_text("body", "Images")
        # FIXME: known to fail for now, needs fixing in registry-image-widgets
        # b.wait_not_visible('.registry-imagestream-push')


if __name__ == '__main__':
    test_main()
